/******************************************************************************
 * Spine Runtimes License Agreement
 * Last updated January 1, 2020. Replaces all prior versions.
 *
 * Copyright (c) 2013-2020, Esoteric Software LLC
 *
 * Integration of the Spine Runtimes into software or otherwise creating
 * derivative works of the Spine Runtimes is permitted under the terms and
 * conditions of Section 2 of the Spine Editor License Agreement:
 * http://esotericsoftware.com/spine-editor-license
 *
 * Otherwise, it is permitted to integrate the Spine Runtimes into software
 * or otherwise create derivative works of the Spine Runtimes (collectively,
 * "Products"), provided that each user of the Products must obtain their own
 * Spine Editor license and redistribution of the Products in any form must
 * include this license and copyright notice.
 *
 * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
 * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

using System.Collections.Generic;
using System.IO;
using UnityEngine;
using System;
#if UNITY_EDITOR
using System.Globalization;
using System.Text.RegularExpressions;
#endif

namespace Spine.Unity {
    public static class SkeletonDataCompatibility {
        #if UNITY_EDITOR
        static readonly int[][] compatibleBinaryVersions = { new[] { 3, 8, 0 } };
        static readonly int[][] compatibleJsonVersions = { new[] { 3, 8, 0 } };

        static bool wasVersionDialogShown = false;
        static readonly Regex jsonVersionRegex = new Regex(@"""spine""\s*:\s*""([^""]+)""", RegexOptions.CultureInvariant);
        #endif

        public enum SourceType { Json, Binary }

        [System.Serializable]
        public class VersionInfo {
            public string rawVersion = null;
            public int[] version = null;
            public SourceType sourceType;
        }

        [System.Serializable]
        public class CompatibilityProblemInfo {
            public VersionInfo actualVersion;
            public int[][] compatibleVersions;
            public string explicitProblemDescription = null;

            public string DescriptionString() {
                if (!string.IsNullOrEmpty(explicitProblemDescription))
                    return explicitProblemDescription;

                string compatibleVersionString = "";
                string optionalOr = null;
                foreach (int[] version in compatibleVersions) {
                    compatibleVersionString += $"{optionalOr}{version[0]}.{version[1]}";
                    optionalOr = " or ";
                }
                return $"无法加载骨架数据。数据版本：{actualVersion.rawVersion}。所需版本：{compatibleVersionString}。\n请使用Spine{compatibleVersionString}重新导出骨架数据，或将运行时更改为版本{actualVersion.version[0]}。{actualVersion.version[1]}";
            }
        }

        #if UNITY_EDITOR
        public static VersionInfo GetVersionInfo(TextAsset asset, out bool isSpineSkeletonData, ref string problemDescription) {
            isSpineSkeletonData = false;
            if (asset == null)
                return null;

            VersionInfo fileVersion = new VersionInfo();
            bool hasBinaryExtension = asset.name.Contains(".skel");
            fileVersion.sourceType = hasBinaryExtension ? SourceType.Binary : SourceType.Json;

            bool isJsonFileByContent = IsJsonFile(asset);
            if (hasBinaryExtension == isJsonFileByContent) {
                if (hasBinaryExtension) {
                    problemDescription = $"读取 {asset.name}失败。扩展名为.skel.bytes”，但内容看起来像“.json”文件。n\n+“导出时是否选择了错误的扩展名？\n";
                } else {
                    problemDescription = $"读取{asset.name}”失败。扩展名为“.json”，但内容看起来像二进制的“skel.bytes”文件。\n您在导出时选择了错误的扩展名吗？n";
                }
                isSpineSkeletonData = false;
                return null;
            }

            if (fileVersion.sourceType == SourceType.Binary) {
                try {
                    using (var memStream = new MemoryStream(asset.bytes)) {
                        fileVersion.rawVersion = SkeletonBinary.GetVersionString(memStream);
                    }
                } catch (System.Exception e) {
                    problemDescription = $"读取“{asset.name}”失败。它可能不是二进制Spine SkeletonData文件。\n{e}";
                    isSpineSkeletonData = false;
                    return null;
                }
            } else {
                Match match = jsonVersionRegex.Match(asset.text);
                if (match != null) {
                    fileVersion.rawVersion = match.Groups[1].Value;
                } else {
                    object obj = Json.Deserialize(new StringReader(asset.text));
                    if (obj == null) {
                        problemDescription = string.Format("'{0}' is not valid JSON.", asset.name);
                        isSpineSkeletonData = false;
                        return null;
                    }

                    var root = obj as Dictionary<string, object>;
                    if (root == null) {
                        problemDescription = string.Format("'{0}' is not compatible JSON. Parser returned an incorrect type while parsing version info.", asset.name);
                        isSpineSkeletonData = false;
                        return null;
                    }

                    if (root.ContainsKey("skeleton")) {
                        var skeletonInfo = (Dictionary<string, object>)root["skeleton"];
                        object jv;
                        skeletonInfo.TryGetValue("spine", out jv);
                        fileVersion.rawVersion = jv as string;
                    }
                }
            }

            if (string.IsNullOrEmpty(fileVersion.rawVersion)) {
                // very likely not a Spine skeleton json file at all. Could be another valid json file, don't report errors.
                isSpineSkeletonData = false;
                return null;
            }

            var versionSplit = fileVersion.rawVersion.Split('.');
            try {
                fileVersion.version = new[] { int.Parse(versionSplit[0], CultureInfo.InvariantCulture), int.Parse(versionSplit[1], CultureInfo.InvariantCulture) };
            } catch (System.Exception e) {
                problemDescription = string.Format("Failed to read version info at skeleton '{0}'. It is likely not a valid Spine SkeletonData file.\n{1}", asset.name, e);
                isSpineSkeletonData = false;
                return null;
            }
            isSpineSkeletonData = true;
            return fileVersion;
        }

        public static bool IsJsonFile(TextAsset file) {
            byte[] content = file.bytes;
            const int maxCharsToCheck = 256;
            int numCharsToCheck = Math.Min(content.Length, maxCharsToCheck);
            int i = 0;
            if (content.Length >= 3 && content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF) // skip potential BOM
                i = 3;
            for (; i < numCharsToCheck; ++i) {
                char c = (char)content[i];
                if (char.IsWhiteSpace(c))
                    continue;
                return c == '{';
            }
            return true;
        }

        public static CompatibilityProblemInfo GetCompatibilityProblemInfo(VersionInfo fileVersion) {
            if (fileVersion == null) {
                return null; // it's most likely not a Spine skeleton file, e.g. another json file. don't report problems.
            }

            CompatibilityProblemInfo info = new CompatibilityProblemInfo();
            info.actualVersion = fileVersion;
            info.compatibleVersions = (fileVersion.sourceType == SourceType.Binary) ? compatibleBinaryVersions : compatibleJsonVersions;

            foreach (var compatibleVersion in info.compatibleVersions) {
                bool majorMatch = fileVersion.version[0] == compatibleVersion[0];
                bool minorMatch = fileVersion.version[1] == compatibleVersion[1];
                if (majorMatch && minorMatch) {
                    return null; // is compatible, thus no problem info returned
                }
            }
            return info;
        }

        public static void DisplayCompatibilityProblem(string descriptionString, TextAsset spineJson) {
            if (!wasVersionDialogShown) {
                wasVersionDialogShown = true;
                UnityEditor.EditorUtility.DisplayDialog("Version mismatch!", descriptionString, "OK");
            }
            Debug.LogError(string.Format("Error importing skeleton '{0}': {1}", spineJson.name, descriptionString), spineJson);
        }
        #endif // UNITY_EDITOR
    }
}
